Repository navigation
Conversation
Metro loads modules lazily, so a define() can run after configure() has started the replay. An outcome for that key emitted the unhandled-key row and then waited for the next launch, even though its definition arrived a moment later. Delivery now holds each outcome whose key has no definition. When define() registers that key, the held outcomes route again through the normal path: they wait for a same-id mutate(), run the handler, and ack. They keep their order per id, also ahead of a later outcome of the id that already waits in the lane. The unhandled-key row is still emitted. An outcome whose key is never defined stays unacknowledged for the next launch, as before. configure() also takes an optional definitions list, the handles that define() returned. Listing a handle imports its module before the replay. In development, a later define() of a key that is not listed warns once. Nothing is rejected, and an unlisted key still receives its outcomes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011y2fcSHHE3i5eQHgATu6Tc
route() now runs its drain loop inline. Before, an empty drainReleased() still yielded one microtask. A define() in that microtask let a newer outcome of the same id and key find its definition and run before the older held one. cancel(id) now drops the held outcomes of that id after native cancel resolves. On a settled entry native forgets the outcomes too, so a later define() would have run a handler for a discarded entry. The README and CHANGELOG now state the order limit across keys, and what a consumer sees after the unhandled-key row. The unknown-key warning says the outcome waits for define() or the next launch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011y2fcSHHE3i5eQHgATu6Tc
dmurphy5
marked this pull request as ready for review
November 9, 2026 20:36
dmurphy5
requested review from
arnonate,
dhruv-dangi-openspace and
elliottkember
November 10, 2026 16:11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cancel(id) now forgets the id's held outcomes before the native call as well as after it. A define() of the key that runs while native cancels no longer runs a handler for the cancelled entry. README rule 8 names the exception from rule 5: when one id has held outcomes under two keys, the key defined first delivers first. New tests: the cancel race, no second state row after a release, and one id with held outcomes under two keys. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Brings in the retry.transient rules (#62-#64) and the iOS chunked-file fix (#68). Conflicts: - CHANGELOG.md: kept master's retry.transient paragraph, then this branch's two paragraphs. - src/index.ts configure(): master's retry check stays first; this branch's definitions check follows it. Both run before any setting changes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Brings in #65, #74, #78 and #80. CHANGELOG.md: kept every entry; master's entries first, then this branch's two (late define() delivers held outcomes, configure({ definitions })). src/__typetests__/define.ts: kept both blocks (this branch's configure({ definitions }) checks and master's takeDiagnostics() checks). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Brings in #71 (mutate() waits, onWarning). CHANGELOG.md: kept every entry; master's entries first, then this branch's two. src/index.ts: createRegistry() gets both this branch's onDefine and #71's warn. The warning for an unlisted late define() now goes through warn(), so it reaches onWarning like every other JS warning (#71's rule). src/__typetests__/define.ts: kept both blocks (configure({ definitions }) and onWarning). src/__tests__/client.test.ts: #71's onWarning test now expects this branch's text for the no-definition warning ("... until define() registers the key, or until the next launch."). Added one test: the unlisted late define() warning goes to onWarning, not console.warn. Both as in the integration branch. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Metro loads modules lazily (
inlineRequires). So adefine()can run afterconfigure().configure()starts the replay of journaled outcomes. An outcome whose key had no definition yet emitted theunhandled-keyrow. Then it waited for the next launch. The definition arrived a moment later, but nothing routed the outcome to it.Diana works around this in openspacelabs/diana#9555. It imports every definition before
configure(). The PR "v11: configure() takes the definitions" fixed it with a breaking contract. This PR fixes it without a breaking change.What changes
Late definitions receive their outcomes. Delivery now holds each outcome whose key has no definition. When
define()registers that key, delivery routes the held outcomes again. They take the normal path: wait for a same-idmutate(), run the handler, then ack.flowchart LR A[Outcome replays] --> B{Key defined?} B -- yes --> C[Handler, then ack] B -- no --> D[Emit unhandled-key row<br/>Hold the outcome] D -- "define() of the key" --> E[Join the id's lane<br/>in original order] E --> C D -- "no define() this session" --> F[Stays unacknowledged<br/>Redelivers next launch]unhandled-keyrow is still emitted, as before.define()that replaces a key does not deliver a held outcome twice.cancel(id)drops the held outcomes of that id before and after native cancel. On a settled entry, native forgets those outcomes too. So a laterdefine(), also one that runs while native cancels, must not run a handler for them. If native cancel rejects, the outcomes were never acknowledged, so native redelivers them at the next launch.unhandled-keyrow is not followed by a newstaterow when the outcome is handled. After the ack, acompletedorcancelledentry leavesgetRequests()without astateevent. Anerrorentry keeps itserrorrow.Optional
configure({ definitions }). It takes the handles thatdefine()returned. Listing a handle imports its module, so itsdefine()runs before the replay. In development, a laterdefine()of a key that is not in the list logs one warning per key:Nothing is rejected. An unlisted key still receives its outcomes, and
mutate()does not wait. Without the option, nothing changes. A new exported type,AnyDefined, names one entry of the list.configure()throws only whendefinitionsis not an array of handles.Where to look
src/delivery.ts:hold(),release(),forget(), and the drain loop inline inroute(). The loop is inline so that noawaitcomes between the last empty check and the key lookup.src/index.ts:onDefine()(dev warning andrelease()), thedefinitionscheck inconfigure(), and theforget()call incancel().src/registry.ts: the newonDefinehook, called afterdefine()registers a key.configure()options table.Known limit
Order holds among the outcomes of one key. If one id has held outcomes under two keys, the key that is defined first delivers first. A held outcome also goes after a later outcome of the same id whose handler started before the define() ran. Outcomes of one id normally share one key.
Test plan
yarn test: 6 suites, 214 tests pass (20 new: 12 inclient.test.ts, 8 indelivery.test.ts).yarn jestinexample/RNBGUExample: 1 suite, 4 tests pass.yarn typecheck, with a new type test forconfigure({ definitions }).yarn lint:ci.release()disabled, 10 new tests fail. With the drain removed fromroute(), the lane-order test fails. Withawait drainReleased()in place of the inline loop, the microtask test fails at depth 1. Withoutforget()incancel(), the cancel test fails. Withforget()only after native cancel, the test that defines the key during the cancel fails.🤖 Generated with Claude Code
https://claude.ai/code/session_011y2fcSHHE3i5eQHgATu6Tc